feat: add --output-format json to test command - #772
Conversation
Adds structured JSON output mode to `pat test` via `--output-format json`. When enabled, all logging is redirected to stderr and a single JSON object is printed to stdout containing summary counts, per-detection test results, failures, invalid specs, and skipped tests. Audit fixes included: - Clamp num_passed to zero to prevent negative counts in JSON summary - Guard setup_data_models print() with logging.error() to avoid stdout pollution - Emit JSON error envelope on early-exit paths (empty specs, no filter match) - Defensive error access in _serialize_function_result for non-dict errors - Use OutputFormat(str, Enum) for Typer-level validation and shell completion - Guard handler.setStream() with isinstance check for non-StreamHandler types - Buffer errored test results in _run_tests for consistent JSON output - Use compact JSON (no indent) for machine-consumable output - Revert cosmetic f-string and cast() changes to reduce diff noise - Add 19 unit tests covering all JSON serialization and output paths Version bump to 1.6.0. Made-with: Cursor
PR SummaryMedium Risk Overview
Written by Cursor Bugbot for commit 743347e. This will update automatically on new commits. Configure here. |
|
the version bump was just so I could distinguish locally, but easy to change |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| """ | ||
| num_passed = max( | ||
| 0, num_detections - (len(failed_tests) + len(invalid_specs) + len(skipped_tests)) | ||
| ) |
There was a problem hiding this comment.
JSON summary passed count wrong with non-detection invalids
Medium Severity
The num_passed formula subtracts len(invalid_specs) from num_detections, but invalid_specs accumulates errors from data models (line 953), detection setup (line 974), and pack validation (line 978). Data model and pack errors are not counted in num_detections (which only counts specs.detections + specs.simple_detections), so the subtraction over-counts and produces an artificially low passed value. The max(0, ...) clamp prevents negatives but doesn't fix the inaccuracy. For example, 5 passing detections with 2 invalid data models would report passed: 3 instead of passed: 5. Since this JSON output is specifically designed for CI/CD programmatic consumption, inaccurate summary counts could cause false alerts or incorrect pipeline decisions.
Additional Locations (1)
|
Closing this in favor of #773, which extends |


Background
Adds structured JSON output to the
testcommand for CI/CD integration and programmatic parsing of test results.This addresses a common need for organizations integrating Panther rule testing into automated workflows — the existing text output is great for humans but fragile to parse programmatically. With
--output-format json, a single JSON object is printed to stdout containing summary counts, per-detection test results, failures, invalid specs, and skipped tests. All logging is redirected to stderr so stdout remains clean, parseable JSON.Closes #634
Usage Examples
Run tests with JSON output:
{ "summary": { "path": "rules/", "total": 1, "passed": 1, "failed": 0, "invalid": 0, "skipped": 0 }, "results": { "Crowdstrike.Detection.Passthrough": [ { "name": "Low Severity Finding", "passed": true, "errored": false, "functions": [ {"name": "rule", "status": "pass", "output": "true"}, {"name": "title", "status": "pass", "output": "Crowdstrike Alert: NGAV on macbook"}, {"name": "severity", "status": "pass", "output": "LOW"}, {"name": "alertContext", "status": "pass", "output": "{\"cid\": \"11111111...\", \"Technique\": \"PUP\"}"} ] }, { "name": "High Severity Finding", "passed": true, "errored": false, "functions": [ {"name": "rule", "status": "pass", "output": "true"}, {"name": "title", "status": "pass", "output": "Crowdstrike Alert: Ransomware on workstation"}, {"name": "severity", "status": "pass", "output": "CRITICAL"} ] } ] }, "failed": {}, "invalid": [], "skipped": [] }Extract just the summary with `jq`:
{ "path": "rules/", "total": 1, "passed": 1, "failed": 0, "invalid": 0, "skipped": 0 }Extract a specific test result with `jq`:
{ "name": "Low Severity Finding", "passed": true, "title": "Crowdstrike Alert: NGAV on macbook", "alertContext": "{\"cid\": \"11111111...\", \"Technique\": \"PUP\"}" }Changes
New CLI option:
--output-format {text,json}on thetestcommand (default:text), backed by anOutputFormat(str, Enum)so Typer provides validation and shell completion out of the boxPANTHER_OUTPUT_FORMATenvironment variableJSON output functions:
_print_json_output()— serializes full test results (summary, per-detection results, failures, invalid specs, skipped tests) as compact JSON to stdout_print_json_error()— emits a valid JSON error envelope on early-exit paths (empty specs, no filter match) so CI consumers always get parseable output_serialize_test_result()/_serialize_function_result()— convert internal dataclass results to JSON-safe dictsRobustness fixes identified during code audit:
num_passedtomax(0, ...)to prevent negative counts wheninvalid_specsincludes non-detection itemsprint()withlogging.error()insetup_data_modelsto avoid stdout pollution in JSON modehandler.setStream()withisinstance(handler, logging.StreamHandler)to avoidAttributeErroron non-stream handlers (e.g.,NullHandler)_serialize_function_resulthandles both dict and string error values_buffer_error_result()ensures tests that throw exceptions still appear in JSONresults, keepingfailedandresultskeys consistentprint()calls insetup_run_testsandrun_testswhen buffering results for JSON outputVersion bump: 1.5.2 → 1.6.0
Testing
19 new unit tests + 81 existing tests — all passing
New tests in
tests/unit/panther_analysis_tool/test_json_output.py:_serialize_function_result— None input, pass/fail/error (dict and string error values), function name stripping_serialize_test_result— passing and errored containers_print_json_output— all-passing, failures, invalid specs, skipped tests, negative-passed clamping, buffered results_print_json_error— valid JSON with errors, empty pathOutputFormatenum — values, string comparison, str subclassExisting tests — all 81
test_main.pytests pass with zero regressionsFormatting —
blackandisortclean